/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.runtime.core.transaction; import org.mule.runtime.api.tx.MuleXaObject; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.api.tx.TransactionException; import org.mule.runtime.core.config.i18n.CoreMessages; import org.mule.runtime.api.i18n.I18nMessageFactory; import org.mule.runtime.api.util.Preconditions; import org.mule.runtime.core.util.xa.XaResourceFactoryHolder; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.transaction.HeuristicRollbackException; import javax.transaction.InvalidTransactionException; import javax.transaction.RollbackException; import javax.transaction.SystemException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; /** * <code>XaTransaction</code> represents an XA transaction in Mule. */ public class XaTransaction extends AbstractTransaction { /** * The inner JTA transaction */ protected Transaction transaction = null; /** * Map of enlisted resources */ private Map<ResourceKey, Object> resources = new HashMap<>(); protected TransactionManager txManager; public XaTransaction(MuleContext context) { super(context); this.txManager = context.getTransactionManager(); } protected void doBegin() throws TransactionException { if (txManager == null) { throw new IllegalStateException(CoreMessages .objectNotRegistered("javax.transaction.TransactionManager", "Transaction Manager").getMessage()); } try { txManager.setTransactionTimeout(getTimeoutInSeconds()); txManager.begin(); synchronized (this) { transaction = txManager.getTransaction(); } } catch (Exception e) { throw new TransactionException(CoreMessages.cannotStartTransaction("XA"), e); } } protected synchronized void doCommit() throws TransactionException { try { /* * JTA spec quotes (parts highlighted by AP), the same applies to both TransactionManager and UserTransaction: * * 3.2.2 Completing a Transaction The TransactionManager.commit method completes the transaction currently associated with * the calling thread. **** * * After the commit method returns, the calling thread is not associated with a transaction. **** * * If the commit method is called when the thread is not associated with any transaction context, the TM throws an * exception. In some implementations, the commit operation is restricted to the transaction originator only. If the calling * thread is not allowed to commit the transaction, the TM throws an exception. The TransactionManager.rollback method rolls * back the transaction associated with the current thread. **** * After the rollback method completes, the thread is associated with no transaction. **** * * And the following block about Transaction (note there's no thread-tx disassociation clause) * * 3.3.3 Transaction Completion The Transaction.commit and Transaction.rollback methods allow the target object to be * comitted or rolled back. The calling thread is not required to have the same transaction associated with the thread. If * the calling thread is not allowed to commit the transaction, the transaction manager throws an exception. * * * So what it meant was that one can't use Transaction.commit()/rollback(), as it doesn't properly disassociate the thread * of execution from the current transaction. There's no JTA API-way to do that after the call, so the thread's transaction * is subject to manual recovery process. Instead TransactionManager or UserTransaction must be used. */ delistResources(); txManager.commit(); } catch (RollbackException e) { throw new TransactionRollbackException(CoreMessages.transactionMarkedForRollback(), e); } catch (HeuristicRollbackException e) { throw new TransactionRollbackException(CoreMessages.transactionMarkedForRollback(), e); } catch (Exception e) { throw new IllegalTransactionStateException(CoreMessages.transactionCommitFailed(), e); } finally { /* * MUST nullify XA ref here, otherwise Transaction.getStatus() doesn't match javax.transaction.Transaction.getStatus(). Must * return STATUS_NO_TRANSACTION and not STATUS_COMMITTED. * * TransactionCoordination unbinds the association immediately on this method's exit. */ this.transaction = null; closeResources(); } } protected void doRollback() throws TransactionRollbackException { try { /* * JTA spec quotes (parts highlighted by AP), the same applies to both TransactionManager and UserTransaction: * * 3.2.2 Completing a Transaction The TransactionManager.commit method completes the transaction currently associated with * the calling thread. **** * * After the commit method returns, the calling thread is not associated with a transaction. **** * * If the commit method is called when the thread is not associated with any transaction context, the TM throws an * exception. In some implementations, the commit operation is restricted to the transaction originator only. If the calling * thread is not allowed to commit the transaction, the TM throws an exception. The TransactionManager.rollback method rolls * back the transaction associated with the current thread. **** * After the rollback method completes, the thread is associated with no transaction. **** * * And the following block about Transaction (note there's no thread-tx disassociation clause) * * 3.3.3 Transaction Completion The Transaction.commit and Transaction.rollback methods allow the target object to be * comitted or rolled back. The calling thread is not required to have the same transaction associated with the thread. If * the calling thread is not allowed to commit the transaction, the transaction manager throws an exception. * * * So what it meant was that one can't use Transaction.commit()/rollback(), as it doesn't properly disassociate the thread * of execution from the current transaction. There's no JTA API-way to do that after the call, so the thread's transaction * is subject to manual recovery process. Instead TransactionManager or UserTransaction must be used. */ // delistResources(); txManager.rollback(); } catch (SystemException e) { throw new TransactionRollbackException(e); } catch (Exception e) { throw new TransactionRollbackException(e); } finally { /* * MUST nullify XA ref here, otherwise Transaction.getStatus() doesn't match javax.transaction.Transaction.getStatus(). Must * return STATUS_NO_TRANSACTION and not STATUS_COMMITTED. * * TransactionCoordination unbinds the association immediately on this method's exit. */ this.transaction = null; closeResources(); } } public synchronized int getStatus() throws TransactionStatusException { if (transaction == null) { return STATUS_NO_TRANSACTION; } try { return transaction.getStatus(); } catch (SystemException e) { throw new TransactionStatusException(e); } } public void setRollbackOnly() { if (transaction == null) { throw new IllegalStateException("Current thread is not associated with a transaction."); } try { synchronized (this) { transaction.setRollbackOnly(); } } catch (SystemException e) { throw (IllegalStateException) new IllegalStateException("Failed to set transaction to rollback only: " + e.getMessage()) .initCause(e); } } public synchronized Object getResource(Object key) { ResourceKey normalizedKey = getResourceEntry(key); return resources.get(normalizedKey); } public synchronized boolean hasResource(Object key) { ResourceKey normalizedKey = getResourceEntry(key); return resources.containsKey(normalizedKey); } /** * @param key Must be the provider of the resource object. i.e. for JDBC it's the XADataSource, for JMS is the * XAConnectionFactory. It can be a wrapper in which case should be a * {@link org.mule.runtime.core.util.xa.XaResourceFactoryHolder} to be able to determine correctly if there's already a * resource for that {@link javax.transaction.xa.XAResource} provider. * @param resource the resource object. It must be an {@link javax.transaction.xa.XAResource} or a * {@link MuleXaObject} * @throws TransactionException */ public synchronized void bindResource(Object key, Object resource) throws TransactionException { ResourceKey normalizedKey = getResourceEntry(key, resource); if (resources.containsKey(key)) { throw new IllegalTransactionStateException(CoreMessages.transactionResourceAlreadyListedForKey(key)); } resources.put(normalizedKey, resource); if (key == null) { logger.error("Key for bound resource " + resource + " is null"); } if (resource instanceof MuleXaObject) { MuleXaObject xaObject = (MuleXaObject) resource; xaObject.enlist(); } else if (resource instanceof XAResource) { enlistResource((XAResource) resource); } else { logger.error("Bound resource " + resource + " is neither a MuleXaObject nor XAResource"); } } // moved here from connection wrapper public boolean enlistResource(XAResource resource) throws TransactionException { TransactionManager txManager = muleContext.getTransactionManager(); try { Transaction jtaTransaction = txManager.getTransaction(); if (jtaTransaction == null) { throw new TransactionException(I18nMessageFactory.createStaticMessage("XATransaction is null")); } resource.setTransactionTimeout(getTimeoutInSeconds()); return jtaTransaction.enlistResource(resource); } catch (RollbackException e) { throw new TransactionException(e); } catch (SystemException e) { throw new TransactionException(e); } catch (XAException e) { throw new TransactionException(e); } } private int getTimeoutInSeconds() { // we need to convert milliseconds timeout to seconds timeout for XA return getTimeout() / 1000; } public boolean delistResource(XAResource resource, int tmflag) throws TransactionException { TransactionManager txManager = muleContext.getTransactionManager(); try { Transaction jtaTransaction = txManager.getTransaction(); if (jtaTransaction == null) { throw new TransactionException(CoreMessages.noJtaTransactionAvailable(Thread.currentThread())); } return jtaTransaction.delistResource(resource, tmflag); } catch (SystemException e) { throw new TransactionException(e); } } public String toString() { return transaction == null ? " <n/a>" : transaction.toString(); } public Transaction getTransaction() { return transaction; } public boolean isXA() { return true; } public void resume() throws TransactionException { TransactionManager txManager = muleContext.getTransactionManager(); if (txManager == null) { throw new IllegalStateException(CoreMessages .objectNotRegistered("javax.transaction.TransactionManager", "Transaction Manager").getMessage()); } try { txManager.resume(transaction); } catch (InvalidTransactionException e) { throw new TransactionException(e); } catch (SystemException e) { throw new TransactionException(e); } } public Transaction suspend() throws TransactionException { TransactionManager txManager = muleContext.getTransactionManager(); if (txManager == null) { throw new IllegalStateException(CoreMessages .objectNotRegistered("javax.transaction.TransactionManager", "Transaction Manager").getMessage()); } try { transaction = txManager.suspend(); } catch (SystemException e) { throw new TransactionException(e); } return transaction; } protected void delistResources() { Iterator i = resources.entrySet().iterator(); while (i.hasNext()) { Map.Entry entry = (Map.Entry) i.next(); final Object xaObject = entry.getValue(); if (xaObject instanceof MuleXaObject) { // there is need for reuse object try { ((MuleXaObject) xaObject).delist(); } catch (Exception e) { logger.error("Failed to delist resource " + xaObject, e); } } } } protected void closeResources() { Iterator i = resources.entrySet().iterator(); while (i.hasNext()) { Map.Entry entry = (Map.Entry) i.next(); final Object value = entry.getValue(); if (value instanceof MuleXaObject) { MuleXaObject xaObject = (MuleXaObject) value; if (!xaObject.isReuseObject()) { try { xaObject.close(); i.remove(); } catch (Exception e) { logger.error("Failed to close resource " + xaObject, e); } } } } } @Override public boolean supports(Object key, Object resource) { return resource instanceof XAResource || resource instanceof MuleXaObject; } private ResourceKey getResourceEntry(Object resourceFactory) { resourceFactory = (resourceFactory instanceof XaResourceFactoryHolder ? ((XaResourceFactoryHolder) resourceFactory).getHoldObject() : resourceFactory); return new ResourceKey(resourceFactory, null); } private ResourceKey getResourceEntry(Object resourceFactory, Object resource) { resourceFactory = (resourceFactory instanceof XaResourceFactoryHolder ? ((XaResourceFactoryHolder) resourceFactory).getHoldObject() : resourceFactory); return new ResourceKey(resourceFactory, resource); } /** * This class is used as key for the resources map since allows us to overcome some bad hashcode implementation of resource * factories such as org.enhydra.jdbc.standard.StandardDataSource. */ private static class ResourceKey { private Object resourceFactory; private Object resource; public ResourceKey(Object resourceFactory) { Preconditions.checkArgument(resourceFactory != null, "resourceFactory cannot be null"); this.resourceFactory = resourceFactory; this.resource = null; } public ResourceKey(Object resourceFactory, Object resource) { this(resourceFactory); this.resource = resource; } public Object getResourceFactory() { return resourceFactory; } public Object getResource() { return resource; } @Override public int hashCode() { return System.identityHashCode(resourceFactory); } @Override public boolean equals(Object obj) { // we use this class internally only so are sure obj is always a ResourceEntry return resourceFactory.equals(((ResourceKey) obj).getResourceFactory()); } } }